<script setup lang="ts">
import {computed, ComputedRef, nextTick, onBeforeUnmount, onMounted, Ref, ref, ShallowRef, shallowRef, watch, WritableComputedRef} from "vue";
import {indentWithTab} from "@codemirror/commands"
import {basicSetup, minimalSetup} from 'codemirror'
import {EditorView, keymap, placeholder, ViewUpdate} from '@codemirror/view'
import {Compartment, EditorSelection, EditorState, Extension, StateEffect, StateField, Text, type Transaction} from '@codemirror/state'
import {indentUnit} from '@codemirror/language';
import {oneDark} from '@codemirror/theme-one-dark'
import {CodeMirrorProps} from "@/components/codemirror/interface";
import {Diagnostic, forceLinting, linter} from "@codemirror/lint";

//主：https://gitcode.com/gh_mirrors/vu/vue-codemirror6/tree/master
//次：https://github.com/surmon-china/vue-codemirror/tree/main
// 开发‘编写脚本’功能参考：https://github.com/dbfu/bp-script-editor
defineOptions({
    name: 'CodeMirror'
})

const props = withDefaults(defineProps<CodeMirrorProps>(), {
    dark: false,
    basic: true,
    minimal: false,
    tab: true,
    tabSize: 4,
    readonly: false,
    disabled: false,
    forceLinting: true,
    scrollbarAtBottom: false
})

const emit = defineEmits(['focus', 'change', 'ready'])

const doc = defineModel<string|Text>();
const editor: Ref<HTMLElement | undefined> = ref();
const view: ShallowRef<EditorView> = shallowRef(new EditorView());

//https://codemirror.net/docs/ref/#view.EditorView.hasFocus
const focus: WritableComputedRef<boolean> = computed({
    get: () => view.value.hasFocus,
    set: f =>  f && view.value.focus(),
});

//https://codemirror.net/docs/ref/#state.EditorSelection
const selection: WritableComputedRef<EditorSelection> = computed({
    get: () => view.value.state.selection,
    set: s => view.value.dispatch({selection: s}),
});

const cursor: WritableComputedRef<number> = computed({
    get: () => view.value.state.selection.main.head,
    set: a => view.value.dispatch({selection: {anchor: a}}),
});

const toJson: WritableComputedRef<Record<string, StateField<any>>> = computed({
    get: () => view.value.state.toJSON(),
    set: j => view.value.setState(EditorState.fromJSON(j)),
});

const length: Ref<number> = ref(0);


// Returns the number of active lint diagnostics in the given state.
// https://codemirror.net/docs/ref/#lint.diagnosticCount
const diagnosticCount: Ref<number> = ref(0);

// Get CodeMirror Extension
const extensions: ComputedRef<Extension[]> = computed(() => {
    // Dynamic Reconfiguration
    // https://codemirror.net/examples/config/
    const language = new Compartment();
    const tabSize = new Compartment();
    if (props.basic && props.minimal) {
        throw '[Vue CodeMirror] Both basic and minimal cannot be specified.';
    }
    // TODO: Ignore previous prop was not changed.
    return [
        props.basic? basicSetup : props.minimal ? minimalSetup : undefined,
        EditorView.updateListener.of((update: ViewUpdate): void => {
            // Emit focus status
            emit('focus', view.value.hasFocus);

            // Update count
            length.value = view.value.state.doc?.length;

            if (update.changes.empty || !update.docChanged) {
                return;
            } else {
                if (props.linter) {
                    // Linter process
                    if (props.forceLinting) {
                        // If forceLinting enabled, first liting.
                        forceLinting(view.value);
                    }
                    // Count diagnostics.
                    diagnosticCount.value = (props.linter(view.value) as readonly Diagnostic[]).length;
                }
                emit('change', update);
            }
        }),
        props.dark? oneDark : undefined,
        EditorView.theme(props.theme ?? {}, {dark: props.dark}),
        props.wrap ? EditorView.lineWrapping : undefined, // 换行
        props.tab ? keymap.of([indentWithTab]) : undefined, // Indent with tab
        props.indentUnit ? indentUnit.of(props.indentUnit) : undefined,
        EditorState.allowMultipleSelections.of(false),
        // EditorState.allowMultipleSelections.of(props.allowMultipleSelections), // Allow Multiple Selections
        props.tabSize ? tabSize.of(EditorState.tabSize.of(props.tabSize)) : undefined, // 缩进单位
        props.phrases ? EditorState.phrases.of(props.phrases) : undefined,
        EditorState.readOnly.of(props.readonly), // 只读
        EditorView.editable.of(!props.disabled), // 可编辑
        props.lineSeparator ? EditorState.lineSeparator.of(props.lineSeparator) : undefined,
        props.lang ? language.of(props.lang) : undefined,  // 设置代码语言
        props.linter ? linter(props.linter, props.linterConfig) : undefined, // Show 🔴 to error line when linter enabled.
        // props.linter && props.gutter ? lintGutter(props.gutterConfig) : undefined,
        props.placeholder ? placeholder(props.placeholder) : undefined, // Placeholder
        ...(Array.isArray(props.extensions)? props.extensions:[props.extensions]), // 追加扩展配置
    ].filter((extension): extension is Extension => !!extension);
});

// 监听配置变化
watch(extensions, (exts: Extension[]) => {
    view.value?.dispatch({
        effects: StateEffect.reconfigure.of(exts),
    });
}, {immediate: true});

// 监听内容变化
watch(() => doc.value, async (value: string) => {
    if (view.value.composing ||  view.value.state.doc.toJSON().join(props.lineSeparator ?? '\n') === value) {
        return;
    }

    // Range Fix ?
    // https://github.com/logue/vue-codemirror6/issues/27
    const isSelectionOutOfRange = !view.value.state.selection.ranges.every(range => range.anchor < (value?.length ?? 0) && range.head < (value?.length ?? 0))

    // Update
    view.value.dispatch({
        changes: {from: 0, to: view.value.state.doc.length, insert: value},
        selection: isSelectionOutOfRange ? {anchor: 0, head: 0} : view.value.state.selection,
        // scrollIntoView: true,
    });
}, {immediate: true});


onMounted(async () => {
    let value;
    if (!editor.value) {
        return;
    }
    if (editor.value.childNodes[0]) {
        value = (editor.value.childNodes[0] as HTMLElement).innerText.trim();
    } else {
        value = doc.value;
    }

    // Register Codemirror
    view.value = new EditorView({
        parent: editor.value,
        state: EditorState.create({doc: value, extensions: extensions.value}),
        dispatch: (tr: Transaction) => {
            view.value.update([tr]);
            if (tr.changes.empty || !tr.docChanged) {
                return;
            }

            // console.log(view.state.doc.toString(), tr);
            // state.toString() is not defined, so use toJSON and toText function to convert string.
            doc.value = tr.state.doc.toString() ?? '';
            // Emit EditorState
            emit('change', tr.state.doc.toString());

            // 滚动条保持在底部
            if (props.scrollbarAtBottom) {
                nextTick(() => {
                    view.value.scrollDOM.scrollTop = view.value.scrollDOM.scrollHeight;
                })
            }
        },
    });

    await nextTick();
    emit('ready', {
        view: view.value, state: view.value.state, container: editor.value,
    });

});

onBeforeUnmount(() => {
    view.value?.destroy()
})
</script>

<template>
    <div ref="editor" class="editor"/>
</template>

<style lang="scss" scoped>
:deep(.cm-editor) {
    height: var(--editor-height, 100%)
}
</style>